﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Events;
using TaskManager.Event;
using TaskManager.UI;

namespace TaskManager.Control
{
    public class TaskManagerViewModel : ViewModelBase
    {
        #region Properties

        public ObservableCollection<TaskViewModel> Tasks { get; set; }

        public ObservableCollection<TaskViewModel> TasksLogger { get; set; }

        private readonly List<KeyValuePair<string, TaskViewModel>> _tasksCache;

        private readonly IEventAggregator _eventAggregator;

        /// <summary>
        /// Clear all tasks from the logger
        /// </summary>
        private DelegateCommand _clearAllCommand;

        #endregion

        #region Constructor

        public TaskManagerViewModel(IEventAggregator eventAggregator)
        {
            _eventAggregator = eventAggregator;
            Tasks = new ObservableCollection<TaskViewModel>();
            TasksLogger = new ObservableCollection<TaskViewModel>();
            _tasksCache = new List<KeyValuePair<string, TaskViewModel>>();

            if (_eventAggregator != null)
                _eventAggregator.GetEvent<TaskEvent>().Subscribe(OnTaskEventPublished);
        }

        #endregion

        #region Event

        public void OnTaskEventPublished(TaskEventArgs args)
        {
            if (args == null)
                return;

            switch (args.Type)
            {
                case TaskEventArgsType.Started:
                    StartTask(args.Task);
                    break;
                case TaskEventArgsType.Updated:
                    UpdateTask(args.TaskId, args.Count);
                    break;
                case TaskEventArgsType.Progressed:
                    ProgressTask(args.TaskId, args.Count);
                    break;
                case TaskEventArgsType.Ended:
                    EndTask(args.TaskId);
                    break;
                case TaskEventArgsType.Renamed:
                    RenameTask(args.TaskId, args.Text);
                    break;
                case TaskEventArgsType.Completed:
                    CompleteTask(args.TaskId);
                    break;
                case TaskEventArgsType.Canceled:
                    CancelTask(args.TaskId);
                    break;
                case TaskEventArgsType.Faulted:
                    FaultTask(args.TaskId, args.Text);
                    break;
                case TaskEventArgsType.Cleared:
                    ClearTask(args.TaskId);
                    break;
                case TaskEventArgsType.ClearedAll:
                    ClearAllTasks();
                    break;
            }
        }

        #endregion

        #region Commands

        public ICommand ClearAllCommand
        {
            get
            {
                if (_clearAllCommand == null)
                {
                    _clearAllCommand = new DelegateCommand(ClearAll, CanClearAll);
                }
                return _clearAllCommand;
            }
        }

        #endregion

        #region Actions

        public bool CanClearAll()
        {
            return TasksLogger != null && TasksLogger.Count > 0;
        }

        /// <summary>
        /// Clear all tasks
        /// </summary>
        public void ClearAll()
        {
            if (_eventAggregator != null)
                _eventAggregator.GetEvent<TaskEvent>().Publish(new TaskEventArgs(TaskEventArgsType.ClearedAll));
        }

        #endregion

        #region Methods

        /// <summary>
        /// Get a task by id int the cache manager
        /// </summary>
        /// <param name="id">id of the task</param>
        /// <param name="task">task</param>
        /// <returns>true if succeed, otherwise false</returns>
        public bool TryGetTaskById(string id, out TaskViewModel task)
        {
            task = _tasksCache.Find(item => string.Equals(item.Key, id)).Value;
            if (task == null)
                return false;
            return true;
        }

        /// <summary>
        /// Send results to the TaskManager at the end of a task
        /// </summary>
        /// <param name="result"></param>
        /// <param name="task"></param>
        public bool SendResults(Task<object> result, TaskViewModel task)
        {
            if (_eventAggregator == null)
                return false;

            // if faulted
            if (result.IsFaulted)
            {
                _eventAggregator.GetEvent<TaskEvent>().Publish(new TaskEventArgs(TaskEventArgsType.Faulted, task.Id,
                                                                                 result.Exception.ToString()));
                return false;
            }

            // if cancelled
            if (result.IsCanceled)
            {
                _eventAggregator.GetEvent<TaskEvent>().Publish(new TaskEventArgs(TaskEventArgsType.Canceled, task.Id));
                return false;
            }

            // if succeed, we start the ending action
            if (result.IsCompleted)
            {
                task.EndAction(result);
                _eventAggregator.GetEvent<TaskEvent>().Publish(new TaskEventArgs(TaskEventArgsType.Completed, task.Id));
                return true;
            }

            return false;
        }

        /// <summary>
        /// Create and start task
        /// </summary>
        /// <param name="task"></param>
        public void StartTask(TaskViewModel task)
        {
            var context = TaskScheduler.FromCurrentSynchronizationContext();
            var tokenSource = new CancellationTokenSource();
            task.TokenSource = tokenSource;

            var newTask = new Task<object>(() =>
                                       {
                                           try
                                           {
                                               object res;

                                               if (!task.IsIndeterminate)
                                               {
                                                   DeterminateTask(task, tokenSource, out res);
                                               }
                                               else
                                               {
                                                   UndeterminateTask(task, tokenSource, out res);
                                               }
                                               return res;

                                           }
                                           catch (Exception e)
                                           {
                                               throw new AggregateException(e);
                                           }
                                       });
            newTask.ContinueWith(result =>
                                     {
                                         if (SendResults(result, task))
                                             task.EndAction(result);
                                     }, tokenSource.Token, TaskContinuationOptions.None, context);

            task.CurrentTask = newTask;
            task.EventAggregator = _eventAggregator;

            _tasksCache.Add(new KeyValuePair<string, TaskViewModel>(task.Id, task));
            Tasks.Add(task);

            try
            {
                task.CurrentTask.Start();
            }
            catch (AggregateException e)
            {
                foreach (var v in e.InnerExceptions)
                    Console.WriteLine(e.Message + " " + v.Message);
            }
        }

        /// <summary>
        /// Start a determitated task
        /// </summary>
        /// <param name="task"></param>
        /// <param name="tokenSource"></param>
        /// <param name="res"></param>
        private void DeterminateTask(TaskViewModel task, CancellationTokenSource tokenSource, out object res)
        {
            res = null;

            try
            {
                while (!task.IsIndeterminate)
                {
                    // Action canceled by user
                    tokenSource.Token.ThrowIfCancellationRequested();

                    if (task.Ticks.Count > 0)
                    {
                        int tick = task.Ticks[0];
                        if (tick < 0)
                        {
                            task.IsIndeterminate = true;
                            return;
                        }

                        res = task.BeginAction();
                        task.Ticks.RemoveAt(0);

                        _eventAggregator.GetEvent<TaskEvent>().Publish(
                             new TaskEventArgs(TaskEventArgsType.Progressed, task.Id, tick));
                    }
                }
            }
            catch (OperationCanceledException)
            {
                if (task.CancelAction != null)
                    task.CancelAction();
            }
        }

        /// <summary>
        /// Start undeterminated task
        /// </summary>
        /// <param name="task"></param>
        /// <param name="tokenSource"></param>
        /// <param name="res"></param>
        private void UndeterminateTask(TaskViewModel task, CancellationTokenSource tokenSource, out object res)
        {
            res = null;

            try
            {
                // Action canceled by user
                tokenSource.Token.ThrowIfCancellationRequested();

                res = task.BeginAction();
            }
            catch (OperationCanceledException)
            {
                if (task.CancelAction != null)
                    task.CancelAction();
            }
        }

        /// <summary>
        /// Rename the task
        /// </summary>
        /// <param name="id">task id</param>
        /// <param name="newName">new name of the task</param>
        public void RenameTask(string id, string newName)
        {
            TaskViewModel task;

            if (string.IsNullOrEmpty(newName))
                return;

            if (TryGetTaskById(id, out task))
            {
                task.Name = newName;
            }
            else
                throw new NullReferenceException("Can't find task in cache list");
        }

        /// <summary>
        /// Start repetitive action
        /// </summary>
        /// <param name="id">task id</param>
        /// <param name="count">cycle count</param>
        public void UpdateTask(string id, int count)
        {
            TaskViewModel task;
            if (TryGetTaskById(id, out task))
            {
                task.Ticks.Add(count);
            }
            else
                throw new NullReferenceException("Can't find task in cache list");
        }

        /// <summary>
        /// End a repetitive task
        /// </summary>
        /// <param name="id">task id</param>
        public void EndTask(string id)
        {
            TaskViewModel task;
            if (TryGetTaskById(id, out task))
            {
                task.Ticks.Add(-1);
            }
            else
                throw new NullReferenceException("Can't find task in cache list");
        }

        /// <summary>
        /// Update status of the task (% progress)
        /// </summary>
        /// <param name="id">task id</param>
        /// <param name="percent">% progress</param>
        public void ProgressTask(string id, int percent)
        {
            TaskViewModel task;
            if (TryGetTaskById(id, out task))
            {
                task.Percent = percent;
            }
            else
                throw new NullReferenceException("Can't find task in cache list");
        }

        /// <summary>
        /// Task finished successfully
        /// </summary>
        /// <param name="id">task id</param>
        public void CompleteTask(string id)
        {
            TaskViewModel task;
            if (TryGetTaskById(id, out task))
            {
                Tasks.Remove(task);

                task.Status = TaskEventArgsType.Completed;
                task.IsCancelPossible = false;
                task.IsClearPossible = true;
                TasksLogger.Add(task);
            }
            else
                throw new NullReferenceException("Can't find task in cache list");
        }

        /// <summary>
        /// Task cancel by user
        /// </summary>
        /// <param name="id">task id</param>
        public void CancelTask(string id)
        {
            TaskViewModel task;
            if (TryGetTaskById(id, out task))
            {
                Tasks.Remove(task);

                task.Status = TaskEventArgsType.Canceled;
                task.IsCancelPossible = false;
                task.IsClearPossible = true;
                TasksLogger.Add(task);
            }
            else
                throw new NullReferenceException("Can't find task in cache list");
        }

        /// <summary>
        /// Task returns errors
        /// </summary>
        /// <param name="id">task id</param>
        /// <param name="error">error message</param>
        public void FaultTask(string id, string error)
        {
            TaskViewModel task;
            if (TryGetTaskById(id, out task))
            {
                Tasks.Remove(task);

                task.Status = TaskEventArgsType.Faulted;
                task.Error = error;
                task.IsCancelPossible = false;
                task.IsClearPossible = true;
                TasksLogger.Add(task);
            }
            else
                throw new NullReferenceException("Can't find task in cache list");
        }

        /// <summary>
        /// Remove task from the logs
        /// </summary>
        /// <param name="id">task id</param>
        public void ClearTask(string id)
        {
            var task = _tasksCache.Find(item => string.Equals(item.Key, id));
            if (task.Key == null || task.Value == null)
                throw new NullReferenceException("Can't find task in cache list");

            TasksLogger.Remove(task.Value);
            _tasksCache.Remove(task);
        }

        /// <summary>
        /// Remove all tasks from the logs
        /// </summary>
        public void ClearAllTasks()
        {
            foreach (var task in TasksLogger)
            {
                var taskToRemove = _tasksCache.Find(item => string.Equals(item.Key, task.Id));
                if (taskToRemove.Key == null || taskToRemove.Value == null)
                    throw new NullReferenceException("Can't find task in cache list");

                _tasksCache.Remove(taskToRemove);
            }

            TasksLogger.Clear();
        }

        #endregion
    }
}
